package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.list.model.*;
import com.atguigu.gmall.list.service.SearchService;
import com.atguigu.gmall.product.client.ProductFeignClient;
import com.atguigu.gmall.product.model.BaseAttrInfo;
import com.atguigu.gmall.product.model.BaseCategoryView;
import com.atguigu.gmall.product.model.BaseTrademark;
import com.atguigu.gmall.product.model.SkuInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author: atguigu
 * @create: 2023-01-06 15:37
 * Slf4j日志规范--接口
 *   log4j-实现
 *   log4j2-实现
 *   logback-实现
 */
@Slf4j
@Service
@SuppressWarnings("all")
public class SearchServiceImpl implements SearchService {

    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 常量：索引库名称
     */
    private static final String INDEX_NAME = "goods";


    /**
     * 1.远程调用商品微服务获取商品相关信息
     * 2.封装索引库商品文档Goods
     * 3.调用ES将文档对象存入索引库
     *
     * @param skuId
     */
    @Override
    public void upperGoods(Long skuId) {
        try {
            //1.创建索引库文档对象：Goods
            Goods goods = new Goods();
            //2.封装商品文档对象Goods中的属性赋值
            //2.1 根据SkuID远程查询SkuInfo商品信息
            SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
            if (skuInfo != null) {
                goods.setId(skuInfo.getId());
                goods.setTitle(skuInfo.getSkuName());
                goods.setCategory3Id(skuInfo.getCategory3Id());
                goods.setPrice(productFeignClient.getSkuPrice(skuId).doubleValue());
                goods.setDefaultImg(skuInfo.getSkuDefaultImg());
                goods.setCreateTime(new Date());
                goods.setCreatedDate(skuInfo.getCreateTime());

                //2.2 根据skuInfo的品牌ID查询品牌信息
                BaseTrademark trademark = productFeignClient.getTrademarkById(skuInfo.getTmId());
                if (trademark != null) {
                    goods.setTmId(trademark.getId());
                    goods.setTmName(trademark.getTmName());
                    goods.setTmLogoUrl(trademark.getLogoUrl());
                }

                //2.3 根据分类ID查询分类信息
                BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
                if (categoryView != null) {
                    goods.setCategory1Id(categoryView.getCategory1Id());
                    goods.setCategory1Name(categoryView.getCategory1Name());

                    goods.setCategory2Id(categoryView.getCategory2Id());
                    goods.setCategory2Name(categoryView.getCategory2Name());

                    goods.setCategory3Id(categoryView.getCategory3Id());
                    goods.setCategory3Name(categoryView.getCategory3Name());
                }

                //2.4 根据skuID查询平台属性以及值
                List<BaseAttrInfo> attrList = productFeignClient.getAttrList(skuId);
                if (!CollectionUtils.isEmpty(attrList)) {
                    List<SearchAttr> attrs = attrList.stream().map(baseAttrInfo -> {
                        SearchAttr searchAttr = new SearchAttr();
                        searchAttr.setAttrId(baseAttrInfo.getId());
                        searchAttr.setAttrName(baseAttrInfo.getAttrName());
                        searchAttr.setAttrValue(baseAttrInfo.getAttrValueList().get(0).getValueName());
                        return searchAttr;
                    }).collect(Collectors.toList());
                    goods.setAttrs(attrs);
                }

                //3.将索引库文档对象存入索引库ES
                //3.1 创建IndexRequest对象 封装索引库名称 文档ID 请求JSON
                IndexRequest indexRequest = new IndexRequest(INDEX_NAME)
                        .id(skuInfo.getId().toString())
                        .source(JSON.toJSONString(goods), XContentType.JSON);
                //3.2 执行保存
                restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);

            }
        } catch (IOException e) {
            e.printStackTrace();
            log.error("商品上架失败：{}", e);
        }
    }

    /**
     * 删除商品文档
     *
     * @param skuId
     */
    @Override
    public void lowerGoods(Long skuId) {
        try {
            DeleteRequest request = new DeleteRequest(
                    INDEX_NAME,
                    skuId.toString());
            restHighLevelClient.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("商品下架失败：{}", e);
        }
    }


    /**
     * 更新商品的热度排名分值
     *
     * @param skuId
     * @return
     */
    @Override
    public void incrHotScore(Long skuId) {
        try {
            //1.更新Redis缓存中商品热点排名分值 分值+1
            //1.1 构建ZSet排名Key
            String hotKey = "hotScore";
            //1.2 调用自增分值+1方法为查询商品增加分值
            String skuIdStr = skuId.toString();
            Double score = redisTemplate.opsForZSet().incrementScore(hotKey, skuIdStr, 1);

            //2.满足更新索引库条件后再更新文档中分值
            if (score % 10 == 0) {
                //2.0 根据文档ID查询商品文档对象
                Map<String, Object> jsonMap = new HashMap<>();
                //注意：索引库中存储是Long类型 将分值有Double转为Long
                jsonMap.put("hotScore", score.longValue());
                //2.1 创建更新文档对象 UpdateRequest
                UpdateRequest updateRequest = new UpdateRequest(INDEX_NAME, skuIdStr);
                updateRequest.doc(jsonMap);
                //2.2 更新文档
                restHighLevelClient.update(
                        updateRequest, RequestOptions.DEFAULT);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 从索引库goods中查询业务数据同时，聚合过滤条件
     *
     * @param searchParam
     * @return
     */
    @Override
    public SearchResponseVo search(SearchParam searchParam) {
        try {
            //一、构建SearchRequest 封装 检索索引库名称  请求体包含：查询方式、分页、高亮、排序、聚合、字段过滤
            SearchRequest searchRequest = this.buiderDSL(searchParam);

            //二、执行检索结果
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

            //三、封装响应结果 返回vo对象
            SearchResponseVo responseVo = this.parseResult(response, searchParam);
            return responseVo;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new SearchResponseVo();
    }


    /**
     * 设置DSL请求路径以及请求体参数
     *
     * @param searchParam
     * @return
     */
    private SearchRequest buiderDSL(SearchParam searchParam) {
        //1.创建SearchRequest对象 指定查询索引库名称
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);

        //2.创建SearchSourceBuilder 封装："query"查询方式 "from","size"分页 "highlight"高亮  "sort"排序 "aggs"聚合
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        //2.1 设置查询方式 请求参数中 "query"
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //2.1.1 设置查询条件中关键字 小米手机->小米 and 手机
        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", searchParam.getKeyword()).operator(Operator.AND);
            boolQueryBuilder.must(matchQueryBuilder);
        }
        //2.1.2 设置查询条件中分类 可能会提交任意一级分类
        if (searchParam.getCategory1Id() != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("category1Id", searchParam.getCategory1Id()));
        }
        if (searchParam.getCategory2Id() != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("category2Id", searchParam.getCategory2Id()));
        }
        if (searchParam.getCategory3Id() != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("category3Id", searchParam.getCategory3Id()));
        }
        //2.1.3 设置查询条件中品牌 &trademark=     1:小米
        if (StringUtils.isNotBlank(searchParam.getTrademark())) {
            String[] split = searchParam.getTrademark().split(":");
            if (split != null && split.length == 2) {
                boolQueryBuilder.filter(QueryBuilders.termQuery("tmId", split[0]));
            }
        }
        //2.1.4 设置查询条件中平台属性 格式多个props=平台属性id:平台属性值:平台属性名称   13:6G:运行内存
        //2.1.4.1 创建多平台属性bool
        BoolQueryBuilder attrBoolQueryBuilder = QueryBuilders.boolQuery();
        String[] props = searchParam.getProps();
        if (props != null && props.length != 0) {
            //2.1.4.2 创建某个平台属性条件 nested 查询 每一组nested包含一组平台属性
            for (String prop : props) {
                //prop 形式 13:6G:运行内存
                String[] split = prop.split(":");
                if (split != null && split.length == 3) {
                    BoolQueryBuilder atrrQueryBuilder = QueryBuilders.boolQuery();
                    atrrQueryBuilder.must(QueryBuilders.termQuery("attrs.attrId", split[0]));
                    atrrQueryBuilder.must(QueryBuilders.termQuery("attrs.attrValue", split[1]));
                    NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", atrrQueryBuilder, ScoreMode.None);
                    attrBoolQueryBuilder.must(nestedQuery);
                }
            }
        }
        boolQueryBuilder.filter(attrBoolQueryBuilder);

        sourceBuilder.query(boolQueryBuilder);

        //2.2 设置分页  请求参数中 "from" "size"
        int from = (searchParam.getPageNo() - 1) * searchParam.getPageSize();
        sourceBuilder.from(from).size(searchParam.getPageSize());

        //2.3 设置高亮 请求参数中 "highlight"  高亮字段 前置标签 后置标签
        //2.3.1 创建高亮对象
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        //2.3.2 设置高亮字段
        highlightBuilder.field("title");
        //2.3.3 设置高亮前置标签
        highlightBuilder.preTags("<font style='color:red'>");
        //2.3.4 设置高亮后置标签
        highlightBuilder.postTags("</font>");
        sourceBuilder.highlighter(highlightBuilder);

        //2.4 设置排序 请求参数中 "sort"  前端：order=排序规则:升序/降序   2:asc
        String order = searchParam.getOrder();
        if (StringUtils.isNotBlank(order)) {
            String[] split = order.split(":");
            if (split != null && split.length == 2) {
                String orderField = "";
                switch (split[0]) {
                    case "1":
                        orderField = "hotScore";
                        break;
                    case "2":
                        orderField = "price";
                        break;
                }
                sourceBuilder.sort(orderField, "asc".equals(split[1]) ? SortOrder.ASC : SortOrder.DESC);
            }
        } else {
            //设置默认排序规则 按照热门商品降序排
            sourceBuilder.sort("hotScore", SortOrder.DESC);
        }

        //2.5 设置过滤字段
        sourceBuilder.fetchSource(new String[]{"id", "defaultImg", "title", "price"}, null);

        //2.6 设置聚合 品牌聚合 平台属性聚合
        //2.6.1 设置品牌聚合（对检索到所有商品进行按照品牌ID进行分组） 三要素：聚合名称；聚合字段；聚合方式
        //2.6.1.1 创建品牌ID聚合对象-加入到 sourceBuilder中"aggs"中
        TermsAggregationBuilder tmIdAgg = AggregationBuilders.terms("tmIdAgg").field("tmId").size(20);
        //2.6.1.2 创建嵌套品牌名称聚合对象
        tmIdAgg.subAggregation(AggregationBuilders.terms("tmNameAgg").field("tmName").size(1));
        //2.6.1.3 创建嵌套品牌LOGO聚合对象
        tmIdAgg.subAggregation(AggregationBuilders.terms("tmLogoUrlAgg").field("tmLogoUrl").size(1));
        sourceBuilder.aggregation(tmIdAgg);

        //2.6.2 设置平台属性聚合 注意：平台属性采用nested聚合
        //2.6.2.1 创建平台属性聚合对象-加入到 sourceBuilder中"aggs"中 平台属性聚合包含 平台属性ID聚合；平台属性ID聚合包含平台属性名称以及值聚合
        NestedAggregationBuilder attrsNestedAgg = AggregationBuilders.nested("attrsAgg", "attrs");
        //2.6.2.2 在平台属性聚合下构建属性ID子聚合
        TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId").size(20);
        //2.6.2.3 在属性ID聚合下构建属性名称子聚合
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName").size(1));
        //2.6.2.4 在属性ID聚合下构建属性值子聚合
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(20));
        attrsNestedAgg.subAggregation(attrIdAgg);
        //2.7 将平台属性聚合加入到"aggs"参数中
        sourceBuilder.aggregation(attrsNestedAgg);

        //3.将SearchSourceBuilder加入到SearchRequest
        return searchRequest.source(sourceBuilder);

    }


    /**
     * 解析ES响应结果
     * 主要从响应JSON中获取分页信息；业务商品列表；聚合到品牌&平台属性
     *
     * @param response    ES响应JSON封装java对象
     * @param searchParam 提交查询条件
     * @return
     */
    private SearchResponseVo parseResult(SearchResponse response, SearchParam searchParam) {
        //1.创建响应结果对象  为给响应对象中属性赋值
        SearchResponseVo responseVo = new SearchResponseVo();
        //2.封装分页信息
        responseVo.setPageNo(searchParam.getPageNo());
        responseVo.setPageSize(searchParam.getPageSize());
        //2.1 从ES返回JSON中获取总记录数
        SearchHits hits = response.getHits();
        long total = hits.getTotalHits().value;
        //2.2 计算总页数 如果总记录数对页大小能整除取则返回取商结果 反之 不能整除取商结果+1
        long totalPage = total % searchParam.getPageSize() == 0 ? (total / searchParam.getPageSize()) : (total / searchParam.getPageSize()) + 1;
        responseVo.setTotal(total);
        responseVo.setTotalPages(totalPage);

        //3.封装响应商品数据-注意解析高亮结果
        SearchHit[] subHits = hits.getHits();
        if (subHits != null && subHits.length > 0) {
            List<Goods> goodsList = new ArrayList<>();
            for (SearchHit subHit : subHits) {
                //3.1获取商品文档JSON字符串
                String goodsJsonStr = subHit.getSourceAsString();
                //3.2将字符串转为商品对象Goods
                Goods goods = JSON.parseObject(goodsJsonStr, Goods.class);
                //3.3获取高亮结果 给 标题重新赋值
                if (!CollectionUtils.isEmpty(subHit.getHighlightFields())) {
                    Text title = subHit.getHighlightFields().get("title").getFragments()[0];
                    goods.setTitle(title.toString());
                }
                goodsList.add(goods);
            }
            responseVo.setGoodsList(goodsList);
        }

        //4. 封装聚合结果 品牌&平台属性
        Map<String, Aggregation> allAggregationMap = response.getAggregations().asMap();
        //4.1 处理响应结果中品牌聚合信息 封装到SearchResponseVo对象
        ParsedLongTerms tmIdAgg = (ParsedLongTerms) allAggregationMap.get("tmIdAgg");
        //4.2 获取品牌ID聚合结果桶
        List<SearchResponseTmVo> tmVoList = tmIdAgg.getBuckets().stream().map(bucket -> {
            SearchResponseTmVo tmVo = new SearchResponseTmVo();
            long tmId = ((Terms.Bucket) bucket).getKeyAsNumber().longValue();
            //4.2.1 通过品牌ID聚合桶获取品牌名称桶
            ParsedStringTerms tmNameAgg = ((Terms.Bucket) bucket).getAggregations().get("tmNameAgg");
            String tmName = tmNameAgg.getBuckets().get(0).getKeyAsString();
            //4.2.2 通过品牌ID聚合桶获取品牌Logo桶
            ParsedStringTerms tmLogoUrlAgg = ((Terms.Bucket) bucket).getAggregations().get("tmLogoUrlAgg");
            String tmLogoUrl = tmLogoUrlAgg.getBuckets().get(0).getKeyAsString();
            tmVo.setTmId(tmId);
            tmVo.setTmName(tmName);
            tmVo.setTmLogoUrl(tmLogoUrl);
            return tmVo;
        }).collect(Collectors.toList());
        responseVo.setTrademarkList(tmVoList);

        //5.处理响应结果中平台属性聚合信息 封装到SearchResponseVo对象
        //5.1 从响应聚合结果中获取平台属性聚合
        ParsedNested attrsAgg = (ParsedNested) allAggregationMap.get("attrsAgg");
        //5.2 从平台属性聚合对象获取聚合桶集合（所有的分组后平台属性ID以及值）
        ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attrIdAgg");
        List<? extends Terms.Bucket> buckets = attrIdAgg.getBuckets();
        //5.3 从上面聚合桶集合进行遍历获取平台属性ID的值--封装响应结果中平台属性集合对象
        if (!CollectionUtils.isEmpty(buckets)) {
            List<SearchResponseAttrVo> attrVos = buckets.stream().map(bucket -> {
                SearchResponseAttrVo attrVo = new SearchResponseAttrVo();
                //获取平台属性ID
                long atrrId = ((Terms.Bucket) bucket).getKeyAsNumber().longValue();
                //获取平台属性名称
                //5.3.1 循环中获取平台名称聚合桶集合-数量只有一个
                ParsedStringTerms attrNameAgg = ((Terms.Bucket) bucket).getAggregations().get("attrNameAgg");
                String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
                //获取平台属性值集合
                //5.3.2 循环中获取平台属性值聚合桶集合
                ParsedStringTerms attrValueAgg = ((Terms.Bucket) bucket).getAggregations().get("attrValueAgg");
                //获取平台属性桶中结果
                List<String> attrValueList = attrValueAgg.getBuckets().stream().map(Terms.Bucket::getKeyAsString).collect(Collectors.toList());
                attrVo.setAttrId(atrrId);
                attrVo.setAttrName(attrName);
                attrVo.setAttrValueList(attrValueList);
                return attrVo;
            }).collect(Collectors.toList());
            //为响应前端检索结果VO设置聚合得到平台属性集合
            responseVo.setAttrsList(attrVos);
        }
        return responseVo;
    }


}
